package co.dzgroup.votacion.conexion;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.StringTokenizer;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import javax.sql.RowSet;
import javax.transaction.UserTransaction;

import org.apache.log4j.Logger;


/**
 * Gestiona la conexion, la ejecucion de sentencias SQL y la obtencion de la
 * metadata de una base de datos.
 * 
 * @author Luis Eduardo Ramírez
 */
public class BaseDatos
{
    public static final String DATA_SOURCE = "jdbc/votacion";
    public static final String DATA_SOURCE_TX = "java:comp/UserTransaction";
    private static final Logger logger = Logger.getLogger(BaseDatos.class);
    /**
     * Objeto Connection interno
     */
    private Connection conexion;
    /**
     * nombre de usuario con el que se conecta a la base de datos
     */
    public String usuario;
    /**
     * Constrase�a de conexion
     */
    public String clave;
    /**
     * JDBC-URL de conexion a la base de datos
     */
    public String url;
    /**
     * Nombre de la clase que implementa el controlador para conectarse al
     * servidor de la base de datos.
     */
    public String controlador;
    /**
     * Cadena que guarda la descripci�n del resultado de la �ltima operaci�n.
     */
    public String resultado = "";
    /**
     * Nombre del algoritmo de encriptamiento para cifrar la contrase�a.
     */
    public String algoritmo;
    public static int MAX_CLAVE = 64;


    /**
     * Crea un objeto BaseDatos en blanco
     */
    public BaseDatos()
    {
    }

    /**
     * Carga la clase identificada por el atributo controlador.
     * 
     * @return true si la operacion se realiz� con �xito
     */
    public boolean cargarControlador()
    {
        boolean ret = false;
        try
        {
            Class.forName(controlador);
            ret = true;
        }
        catch (Exception e)
        {
            logger.error("Controlador:" + controlador + ":FALLO");
            logger.error("Excepcion: " + e.getMessage());
        }
        return ret;
    }

    /**
     * Establece una conexion a la base de datos se requiere el nombre de
     * usuario y la contrase�a para autenticarse.
     * 
     * @param usuario
     * @param clave
     * @return true si la conexion se realiza con exito
     */
    public boolean conectar(String usuario, String clave)
    {
        boolean ret = false;
        if (cargarControlador())
        {
            try
            {
                conexion = DriverManager.getConnection(url, usuario, clave);
                ret = true;
            }
            catch (SQLException sqle)
            {
                logger.error("\nCONEXION: " + url + ":FALLO");
                logger.error("Excepcion:" + sqle);
                resultado = "" + sqle;
            }
        }
        return ret;
    }

    /**
     * Establece una conexion a la base de datos, no requiere nombre de usuario
     * o contrase�a, para llamar a este metodo el constructor debe haber sido
     * invocado con autoAutenticacion en true asi: new
     * BaseDatos(rutaArchivoPropiedades,true)
     * 
     * @return
     */
    public boolean conectar()
    {
        boolean ret = false;
        if (cargarControlador())
        {
            try
            {
                conexion = DriverManager.getConnection(url, usuario, clave);
                ret = true;
            }
            catch (SQLException sqle)
            {
                logger.error("\nCONEXION: " + url + ":FALLO");
                logger.error("Excepcion:" + sqle.getMessage());
                resultado = "" + sqle;
            }
        }
        return ret;
    }

    /**
     * Conecta con un DATA_SOURCE, debe estar definido en el JNDI
     */
    public boolean conectar(String dataSourceName)
    {
        boolean ret = false;
        try
        {
            InitialContext ic = new InitialContext();
            DataSource ds = (DataSource) ic.lookup(dataSourceName);
            conexion = ds.getConnection();
            ret = true;
        }
        catch (NamingException ex)
        {
            System.out.println(ex.getMessage());
            ex.printStackTrace();
        }
        catch (SQLException sqle)
        {
            System.out.println(sqle.getMessage());
            sqle.printStackTrace();
        }
        return ret;
    }

    /**
     * Ejecuta un comando de actualizacion en la base de datos
     * 
     * @param comandoSQL
     *            sentencia sql que se va a ejecutar
     * @return cadena que contiene el n�mero de registros actualizados o la
     *         descripción del error en caso que este ocurra.
     */
    public String ejecutarActualizacion(String comandoSQL)
    {
        String ret = "";
        Statement stmt = null;
        try
        {
            stmt = conexion.createStatement();
            ret = "" + stmt.executeUpdate(comandoSQL);
        }
        catch (SQLException sqle)
        {
            logger.error(sqle.getMessage() + "\nSQL:" + comandoSQL);
            resultado = "" + sqle;
            ret = sqle.getMessage();
        }
        finally
        {
            BaseDatos.cerrarStatement(stmt);
        }
        return ret;
    }

    /**
     * Ejecuta un comando de selecci�n en la base de datos.
     * 
     * @param comandoSQL
     *            sentencia que se va a ejecutar.
     * @return Objeto ResultSet que contiene los datos retornados por la
     *         consulta.
     */
    public ResultSet ejecutarConsulta(String comandoSQL)
    {
        try
        {
            Statement stmt = conexion.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
            return stmt.executeQuery(comandoSQL);
        }
        catch (SQLException sqle)
        {
            logger.error(sqle.getMessage() + "\nSQL:" + comandoSQL);
            resultado = "" + sqle;
            return null;
        }
    }

    /**
     * Ejecuta un comando de selecci�n en la base de datos.
     * 
     * @param comandoSQL
     *            sentencia que se va a ejecutar.
     * @return Objeto ResultSet que contiene los datos retornados por la
     *         consulta.
     */
    public ResultSet ejecutarConsulta(String comandoSQL, int resultSetType)
    {
        try
        {
            Statement stmt = conexion.createStatement(resultSetType, ResultSet.CONCUR_READ_ONLY);
            return stmt.executeQuery(comandoSQL);
        }
        catch (SQLException sqle)
        {
            logger.error(sqle.getMessage() + "\nSQL:" + comandoSQL);
            resultado = "" + sqle;
            return null;
        }
    }

    /**
     * Cierra la conexi�n con la base de datos.
     * 
     * @return retorna true si no se produce ninguna excepci�n durante la
     *         operación.
     */
    public boolean cerrar()
    {
        try
        {
            if (conexion != null)
            {
                conexion.close();
            }
            return true;
        }
        catch (SQLException sqle)
        {
            logger.error("Error cerrando: " + url);
            logger.error(sqle.getMessage());
            resultado = "" + sqle;
            return false;
        }
    }

    /**
     * Establece si despues de cada sentencia de actualizacion se ejecuta commit
     * o s� las transacciones son controladas mediante el uso de commit y
     * rollback.
     * 
     * @param valor
     */
    public void autoCommit(boolean valor)
    {
        try
        {
            conexion.setAutoCommit(valor);
        }
        catch (SQLException e)
        {
            logger.error("BaseDatos.autoCommit(): " + e.getCause() + " - " + e.getMessage());
            // use Options | File Templates.
            resultado = "" + e;
        }
    }

    /**
     * Ejecuta commit sobre la transacción, este comando debe invocarse si
     * previamente fue puesto autocommit en false.
     */
    public void commit()
    {
        try
        {
            conexion.commit();
        }
        catch (SQLException e)
        {
            logger.error("BaseDatos.commit(): " + e.getCause() + " - " + e.getMessage());
            // use Options | File Templates.
            resultado = "" + e;
        }
    }

    /**
     * Deshace las sentencias de actualización invocadas desde que se colocó
     * autocommit en false.
     * 
     */
    public void rollback()
    {
        try
        {
            conexion.rollback();
        }
        catch (SQLException e)
        {
            logger.error("BaseDatos.commit(): " + e.getCause() + " - " + e.getMessage());
            // use Options | File Templates.
            resultado = "" + e;
        }
    }

    /**
     * Crea un objeto PreparedStatement a partir de una consulta sql
     * 
     * @param sentencia
     * @return objeto PreparedStatement
     * 
     * @see java.sql.PreparedStatement
     */
    public PreparedStatement crearSentencia(String sentencia)
    {
        try
        {
            return conexion.prepareStatement(sentencia);
        }
        catch (SQLException sqle)
        {
            System.out.println("Excepcion: " + sqle);
            resultado = "" + sqle;
            return null;
        }
    }

    /**
     * Crea un objeto PreparedStatement a partir de una consulta sql
     * especificando la categoria de ResultSet
     * 
     * @param sentencia
     * @param tipo
     * @param concurrencia
     * @return
     */
    public PreparedStatement crearSentencia(String sentencia, int tipo, int concurrencia)
    {
        try
        {
            return conexion.prepareStatement(sentencia, tipo, concurrencia);
        }
        catch (SQLException sqle)
        {
            System.out.println("Excepcion: " + sqle);
            resultado = "" + sqle;
            return null;
        }
    }

    /**
     * Crea un objeto Statement en blanco.
     * 
     * @return Objeto Statement generado
     * @see java.sql.Statement
     */
    public Statement crearSentencia()
    {
        try
        {
            return conexion.createStatement();
        }
        catch (SQLException sqle)
        {
            logger.error("\nExcepcion: " + sqle);
            resultado = "" + sqle;
            return null;
        }
    }

    /**
     * Devuelve el n�mero siguiente de un campo consecutivo
     * 
     * @param tabla
     *            Tabla de la base de datos
     * @param campo
     *            Campo al que se le genera el consecutivo.
     * @return Número generado.
     * 
     */
    public int consecutivo(String tabla, String campo)
    {
        PreparedStatement sentencia = null;
        int numero = -1;
        try
        {
            String sql = "SELECT MAX(" + campo + ")AS numero FROM " + tabla;
            sentencia = crearSentencia(sql);
            ResultSet rs = sentencia.executeQuery();
            if (rs.next())
                numero = rs.getInt("numero") + 1;
            else
                numero = 1; // no hay registros
        }
        catch (SQLException sqle)
        {
            logger.error("\n" + sqle.getMessage() + "\nSQL:" + sentencia);
            resultado = "" + sqle;
        }
        return numero;
    }

    /**
     * Devuelve el n�mero siguiente de un campo n�merico
     * 
     * @param tabla
     *            Tabla de la base de datos
     * @param campo
     *            Campo al que se le genera el consecutivo.
     * @return N�mero generado.
     * 
     */
    public int consecutivoConClave(String tabla, String campo, String campoValidacion, String valorValidacion)
    {
        PreparedStatement sentencia = null;
        int numero = -1;
        try
        {
            String sql = "SELECT MAX(" + campo + ") AS numero FROM " + tabla + " WHERE " + campoValidacion + "=" + valorValidacion;
            sentencia = crearSentencia(sql);
            ResultSet rs = sentencia.executeQuery();
            if (rs.next())
                numero = rs.getInt("numero") + 1;
            else
                numero = 1; // no hay registros
        }
        catch (SQLException sqle)
        {
            logger.error("\n" + sqle.getMessage() + "\nSQL:" + sentencia);
            resultado = "" + sqle;
        }
        return numero;
    }

    /**
     * Devuelve el nombre de la maquina a la que est� conectado.
     * 
     * @return String con el nombre de la maquina.
     */
    public String servidorDeTrabajo()
    {
        String ret = "";
        try
        {
            String url = conexion.getMetaData().getURL();
            StringTokenizer st = new StringTokenizer(url, "/");
            st.nextToken();
            String ip = st.nextToken();
            InetAddress i = InetAddress.getByName(ip);
            st = new StringTokenizer(i.getHostName(), ".");
            ret = st.nextToken();
        }
        catch (UnknownHostException e1)
        {
            ret = "";
        }
        catch (SQLException e)
        {
            ret = "";
        }
        return ret;
    }

    /**
     * Devuelve el nombre de la base de datos a la que est� conectado.
     * 
     * @return
     */
    public String baseDatosDeTrabajo()
    {
        String ret = "";
        try
        {
            String url = conexion.getMetaData().getURL();
            ret = url.substring(url.lastIndexOf("/") + 1, url.lastIndexOf("?") == -1 ? url.length() : url.lastIndexOf("?"));
        }
        catch (SQLException e)
        {
            logger.error("BaseDatos.baseDatosDeTrabajo(): " + e.getCause() + " - " + e.getMessage());
            resultado = "" + e;
        }
        return ret;
    }

    /**
     * Cuenta cuantos registros contiene un ResultSet
     * 
     * @param rs
     *            ResultSet
     * @return N�mero de registros que tiene el ResultSet.
     * @throws SQLException
     */
    public int contarRegistros(ResultSet rs) throws SQLException
    {
        int conteo = 0;
        if (rs != null)
        {
            while (rs.next())
            {
                conteo++;
            }
            rs.beforeFirst();
        }
        return conteo;
    }

    /**
     * Obtiene la metadata de la base de datos a la que esta conectado
     * 
     * @return objeto de la clase DatabaseMetaData
     */
    public DatabaseMetaData getMetaData()
    {
        DatabaseMetaData ret = null;
        try
        {
            ret = conexion.getMetaData();
        }
        catch (SQLException e)
        {
            logger.error("" + e.getMessage());
            resultado = "" + e;
        }
        return ret;
    }

    /**
     * Obtiene la llave primaria de una tabla
     * 
     * @param TABLA
     *            tabla de la cual se quiere obtener la llave primaria
     * @return ResultSet que contiene los campos que conforman la llave
     *         primaria.
     */
    public ResultSet getPrimaryKey(String TABLA)
    {
        try
        {
            DatabaseMetaData dbmt = getMetaData();
            return dbmt.getPrimaryKeys(conexion.getCatalog(), "public", TABLA);
        }
        catch (Exception exc)
        {
            logger.error("" + exc.getMessage());
            return null;
        }
    }

    /**
     * Retorna informaci�n acerca de las columnas de una tabla
     * 
     * @param TABLA
     * @return ResultSet que contiene informaci�n sobre cada una de las columnas
     *         de la tabla.
     */
    public ResultSet getColumnsAtributes(String TABLA)
    {
        try
        {
            DatabaseMetaData dbmt = getMetaData();
            return dbmt.getColumns(conexion.getCatalog(), "public", TABLA, null);
        }
        catch (Exception exc)
        {
            logger.error("" + exc.getMessage());
            return null;
        }
    }

    /**
     * Funcion especifica de Postgresql para consultar los campos de una tabla.
     * 
     * @param tabla
     * @return ResultSet que contiene en cada fila un campo de la tabla de la
     *         siguiente forma [column_number, column_name]
     */
    public ResultSet getColumnsAtributesPostgres(String tabla)
    {
        ResultSet rs = null;
        String sql = "select atributo.attnum as column_number ,atributo.attname as column_name " + "from pg_namespace esquema, pg_class tabla, pg_attribute atributo "
                     + "where esquema.oid = tabla.relnamespace " + "and tabla.oid = atributo.attrelid " + "and esquema.nspname = 'public' " + "and tabla.relkind = 'r' " + "and tabla.relname = '"
                     + tabla + "' " + "and atributo.attisdropped = false " + "and atributo.attislocal = true " + "and atributo.attnum > 0 ";
        rs = ejecutarConsulta(sql);
        return rs;
    }

    /**
     * Obtiene informaci�n de las llaves foraneas que posee una tabla.
     * 
     * @param TABLA
     * @return ResultSet que posee informaci�n relativa a cada una de las llaves
     *         foraneas.
     */
    public ResultSet getForeignKey(String TABLA)
    {
        try
        {
            DatabaseMetaData dbmt = getMetaData();
            return dbmt.getImportedKeys(conexion.getCatalog(), "public", TABLA);
        }
        catch (Exception exc)
        {
            logger.error("" + exc.getMessage());
            return null;
        }
    }

    /**
     * Obtiene informaci�n acerca de todas las columnas de una tabla.
     * 
     * @param tabla
     * @return ResultSet que contiene informaci�n relativa a cada una de las
     *         columnas de la tabla.
     */
    public ResultSet camposDeTabla(String tabla)
    {
        ResultSet resultado = null;
        DatabaseMetaData bd_metadata = null;
        bd_metadata = getMetaData();
        try
        {
            resultado = bd_metadata.getColumns(servidorDeTrabajo(), "public", tabla, "");
        }
        catch (SQLException e)
        {
            logger.error("" + e.getMessage());
            this.resultado = "" + e;
        }
        return resultado;
    }

    /**
     * Verifica que un campo de una tabla no posea cierto valor
     * 
     * @param base
     *            Interfaz con la base de datos
     * @param tabla
     * @param nombreCampo
     *            nombre del campo que se quiere verificar
     * @param valor
     *            valor que se quiere verificar
     * @param llavePrimaria
     *            campo que es la llave primaria de la tabla
     * @param valorLlave
     *            valor de la llave primaria
     * 
     * @return true si el valor solicitado existe.
     */
    public boolean existeCampoCadenaIgual(BaseDatos base, String tabla, String nombreCampo, String valor, String llavePrimaria, String valorLlave)
    {
        boolean ret = false;
        ResultSet resultado = base.ejecutarConsulta("SELECT " + nombreCampo + " FROM " + tabla + " WHERE " + nombreCampo + "='" + valor + "' AND " + llavePrimaria + "<>" + valorLlave);
        try
        {
            if (resultado.next())
                ret = true;
        }
        catch (SQLException e)
        {
        }
        return ret;
    }

    /**
     * Igual que seleccionParametrizacion pero no aplica ningun tipo de case
     * (upperCase, lowerCase, titleCase)
     * 
     * @author inf_mauror
     * 
     */
    public String seleccionParametrizacionSinCase(String consulta, String dato, String campoVisible, String campoValor)
    {
        String ret = "";
        String opcion = "";
        ResultSet datos = ejecutarConsulta(consulta);
        try
        {
            while (datos.next())
            {
                String nombre = datos.getString(campoVisible).trim();
                String id = datos.getString(campoValor).trim();
                opcion = "";
                if (dato.equals(id))
                {
                    opcion = "selected";
                }
                ret += "<option value=" + id + " " + opcion + ">" + nombre + "</option>\n";
            }
        }
        catch (SQLException e)
        {
        }
        finally
        {
            BaseDatos.cerrarResultSet(datos);
        }
        return ret;
    }


    public static void cerrarRowSet(RowSet rs)
    {
        if (rs != null)
        {
            try
            {
                rs.close();
            }
            catch (SQLException sqle)
            {
                sqle.printStackTrace();
            }
        }
    }

    public static synchronized void cerrarResultSet(ResultSet rs)
    {
        if (rs != null)
        {
            try
            {
                rs.close();
            }
            catch (SQLException sqle)
            {
                sqle.printStackTrace();
            }
        }
    }

    public static synchronized void cerrarStatement(Statement stmt)
    {
        if (stmt != null)
        {
            try
            {
                stmt.close();
            }
            catch (SQLException sqle)
            {
                sqle.printStackTrace();
            }
        }
    }

    public Connection getConexion()
    {
        return conexion;
    }

    /**
     * Transacciones con JTA
     * 
     * @author inf_santiz
     * @return
     * @throws Exception
     */
    public UserTransaction getTransaction() throws Exception
    {
        Context ctx = new InitialContext();
        UserTransaction transaction = (UserTransaction) ctx.lookup(DATA_SOURCE_TX);
        return transaction;
    }

    public void commit(UserTransaction ut) throws Exception
    {
        try
        {
            logger.debug("transaction in thread " + Thread.currentThread().getId() + ": commited");
            ut.commit();
        }
        catch (NoSuchMethodError nse)
        {
            logger.debug(nse.getMessage());
        }
    }

    public void rollback(UserTransaction ut) throws Exception
    {
        try
        {
            logger.debug("transaction in thread " + Thread.currentThread().getId() + ": rolledback");
            ut.rollback();
        }
        catch (NoSuchMethodError nse)
        {
            logger.debug(nse.getMessage());
        }
    }
}
